Advanced Topics in Multithreading
This section explores advanced multithreading concepts that are essential for building robust and scalable applications. We’ll cover:
- Deadlocks: Causes and prevention.
ThreadLocal: Thread-specific data storage.- ForkJoin Framework: Parallel processing using
ForkJoinPool.
1. Deadlocks
A deadlock occurs when two or more threads are waiting for each other to release resources, resulting in a standstill.
Example: Detecting and Resolving Deadlocks
public class DeadlockExample {
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
// Thread 1
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Thread 1 interrupted.");
}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and resource 2.");
}
}
});
// Thread 2
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Thread 2 interrupted.");
}
System.out.println("Thread 2: Waiting for resource 1...");
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 2 and resource 1.");
}
}
});
thread1.start();
thread2.start();
}
}
Output (Potential Deadlock):
Thread 1: Holding resource 1...
Thread 2: Holding resource 2...
Thread 1: Waiting for resource 2...
Thread 2: Waiting for resource 1...
Explanation:
- Both threads hold one resource and wait for the other, causing a deadlock.
- To prevent deadlocks, acquire resources in a consistent order or use timeouts.
2. ThreadLocal
ThreadLocal provides thread-specific storage, ensuring that each thread has its own independent copy of a variable.
Example: Using ThreadLocal
public class ThreadLocalExample {
// Create a ThreadLocal variable
private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
// Get the current thread's value
int value = threadLocalValue.get();
System.out.println(Thread.currentThread().getName() + " initial value: " + value);
// Update the value
threadLocalValue.set(value + 1);
System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocalValue.get());
};
// Create multiple threads
Thread thread1 = new Thread(task, "Thread-1");
Thread thread2 = new Thread(task, "Thread-2");
thread1.start();
thread2.start();
}
}
Output:
Thread-1 initial value: 0
Thread-2 initial value: 0
Thread-1 updated value: 1
Thread-2 updated value: 1
Explanation:
- Each thread maintains its own copy of the
threadLocalValuevariable. - Changes made by one thread do not affect the value seen by other threads.
3. ForkJoin Framework
The ForkJoin Framework is designed for parallel processing of tasks. It uses a ForkJoinPool to divide tasks into smaller subtasks and execute them concurrently.
Example: Parallel Processing with ForkJoinPool
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
public class ForkJoinExample {
public static void main(String[] args) {
// Create a ForkJoinPool
ForkJoinPool pool = new ForkJoinPool();
// Define a task to compute the sum of numbers from 1 to 100
SumTask task = new SumTask(1, 100);
// Execute the task
int result = pool.invoke(task);
System.out.println("Sum of numbers from 1 to 100: " + result);
}
}
// RecursiveTask to compute the sum of numbers
class SumTask extends RecursiveTask<Integer> {
private int start;
private int end;
public SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= 10) {
// Base case: Compute the sum directly
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
// Divide the task into two subtasks
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(start, mid);
SumTask rightTask = new SumTask(mid + 1, end);
// Fork the left task and compute the right task
leftTask.fork();
int rightResult = rightTask.compute();
int leftResult = leftTask.join();
// Combine results
return leftResult + rightResult;
}
}
}
Output:
Sum of numbers from 1 to 100: 5050
Explanation:
- The
ForkJoinPooldivides the task of summing numbers into smaller subtasks. - Each subtask computes its portion of the sum, and the results are combined.
Key Takeaways
- Deadlocks: Avoid deadlocks by acquiring resources in a consistent order or using timeouts.
ThreadLocal: UseThreadLocalfor thread-specific data storage.- ForkJoin Framework: Use the
ForkJoinPoolfor parallel processing of tasks.